﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.IO;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;

namespace TCPSockets
{
    /// <summary>Класс, реализующий клиентскую программу для TCP соединения</summary>
    public class CTCPClient : IDisposable, Interfaces.IClient
    {
        /* - Описание событий, делегатов и классов аркументов, возращаемых событиями --------------------------------------------------------- */

        #region События

        /// <summary>Событие соединения клиента с сервером</summary>
        public event ClientEvents.Handlers.OnConnectedEventHandler OnConnected;

        /// <summary>Позволяет легко вызвать событие "при присоединении"</summary>
        protected virtual void InvokeConnectedEvent()
        {
            if(OnConnected != null)
                OnConnected.Invoke(this, new EventArgs());
        }

        /// <summary>Событие, возникающие при потери связи</summary>
        public event ClientEvents.Handlers.OnDisconnectedEventHandler OnDisconnected;

        /// <summary>Позволяет легко вызвать событие "при отсоединении"</summary>
        protected virtual void InvokeDisconnectedEvent()
        {
            if(OnDisconnected != null)
                OnDisconnected.Invoke(this, EventArgs.Empty);
        }

        /// <summary>Событие, возникающие при ошибке</summary>
        public event ClientEvents.Handlers.OnErrorEventHandler OnError;

        /// <summary>Позволяет легко вызвать событие "при ошибке"</summary>
        /// <param name="Error">Возникшая ошибка</param>
        protected virtual void InvokeErrorEvent(Exception Error)
        {
            if(OnError != null)
                OnError.Invoke(this, Error);
            else
                throw Error;
        }

        /// <summary>Событие, возникающие при появлении данных в сетевом потоке</summary>
        public event ClientEvents.Handlers.OnReceiveDataEventHandler OnReceiveData;

        /// <summary>Позволяет легко вызвать событие "при получении данных" с параметром в виде самих данных</summary>
        /// <param name="Data">Полученные данные</param>
        protected virtual void InvokeReceiveDataEvent(byte[] Data)
        {
            ClientEvents.Args.DataEventArgs Args = new ClientEvents.Args.DataEventArgs(Data, fDataEncoding, fDataFormatter);

            if(OnReceiveData != null)
                OnReceiveData.Invoke(this, Args);
        }

        /// <summary>Событие, возникающие при благополучной отправке</summary>
        public event ClientEvents.Handlers.OnDataSendEventHandler OnDataSend;

        protected virtual void InvokeDataSendEvent(byte[] Data)
        {
            if(OnDataSend != null)
                OnDataSend.Invoke(this,
                    new ClientEvents.Args.DataEventArgs(Data, fDataEncoding, fDataFormatter));
        }

        #endregion

        /* - Атрибуты клиента ---------------------------------------------------------------------------------------------------------------- */

        #region Атрибуты

        /// <summary>
        /// Основной объект клиента
        /// </summary>
        /// <remarks>
        /// Инкапсулирует работу с сетевым потоком данных и с сокетами
        /// В интерфейс класса не входит
        /// </remarks>
        protected TcpClient f_Client;

        /// <summary>
        /// Сетевой поток данных
        /// </summary>
        protected NetworkStream f_ClientStream;
        /// <summary>
        /// Объект для чтения из сетевого потока данных
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected BinaryReader f_Reader;
        /// <summary>
        /// Объект для записи данных в поток
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected BinaryWriter f_Writer;

        protected Encoding fDataEncoding = Encoding.ASCII;

        /// <summary>
        /// Строка с именем удалённого Host'а
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected string fHost = "127.0.0.1";
        /// <summary>
        /// Переменная целого типа, указывающай удалённый порт
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected int fPort = 8080;
        /// <summary>
        /// главноый поток клиента. В нём происходит обработка поступающих данных и возникают события "при получении данных" и "при потери связи". Так же в нём проводится проверка на подключение к удалённому серверу.
        /// </summary>
        /// <remarks>
        /// Для внутриклассового использования.
        /// В интерфейс класса не входит
        /// </remarks>
        protected Thread f_CheckThread = null;
        protected int fSleepTime = 100;
        /// <summary>
        /// Bool'евская переменная, отражающая статус клиента - подключён / не подключён
        /// </summary>
        /// <remarks>В интерфейс класса не входит</remarks>
        protected bool f_Connected = false;

        protected IFormatter fDataFormatter = new BinaryFormatter();

        #endregion

        /* - Свойства ------------------------------------------------------------------------------------------------------------------------ */

        #region Свойства

        /// <summary>Свойство клиента, отражающее его статус</summary>
        /// <value>Подключён / не подключён</value>
        public bool Connected
        {
            get { return f_Connected; }
            set { if(value) Start(); else Stop(); }
        }

        /// <summary>Удалённый Host</summary>
        /// <remarks>Можно изменять во время работы слиента. Клиент отключится от сервера, изменит настройку и попытается переподключиться к указанному Host'у</remarks>
        /// <value>Имеет строковый тип. Может быть указан в виде IP адреса.</value>
        public string Host
        {
            get { return fHost; }
            set
            {
                //Если пытаемся установить уже текущий Host, то возврат
                if(value == fHost) return;
                //сохраняем текущее состояние клиента
                bool Temp = Connected;
                //Отключаемся
                Connected = false;
                //Присваеваем значение
                fHost = value;
                //Восстанавливаем состояние
                Connected = Temp;
            }
        }

        public IPAddress Address
        {
            get
            {
                IPAddress Address;
                if(IPAddress.TryParse(Host, out Address))
                    return Address;
                return null;
            }
        }

        /// <summary>Удалённый порт</summary>
        /// <remarks>
        /// В случае задания недопустимого значения будет сгенерировано исключение
        /// Может быть изменино во время работы клиента. Клиент отключиться от сервера, изменит настройку и попытается установить соединение с новым сервером.
        /// </remarks>
        /// <value>принимает значения целого типа в пределах от 1 до 2^16</value>
        public int Port
        {
            get { return fPort; }
            set
            {
                //Если пытаемся установить текущее значение порта, то возврат
                if(value == fPort) return;
                //Если значение недопустимо, то генерируем исключение
                if(value < 1 || value > 65535)
                    throw new ArgumentOutOfRangeException("Port", value, "Порт должен быть в пределах от 1 до 65535, а указан " + value.ToString());

                //Сохраняем текущее соятояние
                bool Temp = Connected;
                //Отключяемся
                Connected = false;
                //Присваеваем новое значение порта
                fPort = value;
                //Восстанавливаем исходное состояние
                Connected = Temp;
            }
        }

        public Encoding DataEncoding { get { return fDataEncoding; } set { fDataEncoding = value; } }
        public NetworkStream SocketStream { get { return f_ClientStream; } }
        public IFormatter DataFormatter
        {
            get { return this.fDataFormatter; }
            set
            {
                if(value == null) throw new NullReferenceException("Не задан сериализатор!");
                this.fDataFormatter = value;
            }
        }

        #endregion

        /* - Время жизни объекта (конструктор / диструктор) ---------------------------------------------------------------------------------- */

        #region Конструктор / диструктор

        /// <summary>
        /// Конструктор без параметров.
        /// </summary>
        /// <remarks>
        /// предпологается, что все необходимые параметры будут заданы во время работы с созданным объектом.
        /// Изначальные значения
        /// Host = "127.0.0.1"
        /// Port = 8080
        /// </remarks>
        public CTCPClient()
        {

        }

        public CTCPClient(string Host)
        {
            string[] Data = Host.Split(':');
            fHost = Data[0];
            if(Data.Length > 1)
                if(!int.TryParse(Data[1], out fPort))
                    throw new ArgumentException("Неверный формат строки. Должен быть: \"Host\", или \"Host\":Port");

        }

        /// <summary>
        /// Конструктор клиента по указанным Host'у и порту
        /// </summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        public CTCPClient(string Host, int Port)
        {
            //Если значение порта недопустимо, то генерируем исключительную ситуацию
            if(Port < 1 || Port > 65535)
                throw new ArgumentOutOfRangeException("Port", Port, "Порт должен быть в пределах от 1 до 65535, а указан " + Port.ToString());
            fHost = Host;
            fPort = Port;
        }

        /// <summary>
        /// Конструктор клиента по указанному адресу сервера, порту и состоянию клиента после его создания.
        /// </summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        /// <param name="Enabel">Состояние подключения после создания. True - клиент будет запущен непосредственно по завершении работы конструктора</param>
        public CTCPClient(string Host, int Port, bool Enabel)
        {
            //Если значение порта недопустимо, то генерируем исключительную ситуацию
            if(Port < 1 || Port > 65535)
                throw new ArgumentOutOfRangeException("Port", Port, "Порт должен быть в пределах от 1 до 65535, а указан " + Port.ToString());
            fHost = Host;
            fPort = Port;
            if(Enabel) this.Connected = true;
        }

        /// <summary>
        /// Конструктор клиена по указанному объекту типа TcpLient
        /// </summary>
        /// <remarks>Нужен для использования в серверной части</remarks>
        /// <param name="Client">Объект класса TcpClient</param>
        public CTCPClient(TcpClient Client)
        {
            f_Client = Client;
            f_Connected = true;
            //проверяем, получен ли от сервера подключённый клиент. Если соединение отсутствует, то возврат
            if(!Connected) return;

            //Создаём сетевой поток
            f_ClientStream = new NetworkStream(f_Client.Client);
            //f_ClientStream.ReadTimeout = 250;
            //...объект для чтения данных из потока
            f_Reader = new BinaryReader(f_ClientStream);
            //...объект для записи данных в поток
            f_Writer = new BinaryWriter(f_ClientStream);

            //Выясняем удалённый адрес Host'а
            fHost = f_Client.Client.RemoteEndPoint.ToString().Split(':')[0];
            //Выясняем порт удалённого клиента
            fPort = Int32.Parse(f_Client.Client.RemoteEndPoint.ToString().Split(':')[1]);

            //Создаём основной поток клиента
            f_CheckThread = new Thread(new ThreadStart(CheckConnection));
            //Запускаем основной поток клиента
            f_CheckThread.Start();
            //Сообщаем о подключении
            InvokeConnectedEvent();
        }

        #endregion

        /* - Методы запуска / остановки ------------------------------------------------------------------------------------------------------ */

        #region Запуск / остановка

        /// <summary>Метод установления соединения у удалённым сервером</summary>
        /// <param name="Host">Адрес удалённого сервера</param>
        /// <param name="Port">Порт</param>
        public bool Connect(string Host, int Port)
        {
            //Если подключение к указанному адресу и порту уже выполнено, то возврат
            if(Connected && Host == fHost && Port == fPort) return true;

            //В случае, если клиент был подключён, отключаемся
            Connected = false;

            //Устанавливаем новые параметры соединения
            this.Host = Host;
            this.Port = Port;

            //Подключаемся к новой точке
            Connected = true;

            return Connected;
        }

        /// <summary>Метод запуска клиента</summary>
        protected void Start()
        {
            //Если соединение уже установлено, то возврат
            if(Connected) return;

            try
            {
                //Устанавливаем флаг, свидетельствющий о наличии соединения
                f_Connected = true;

                //Создаём нового клиента
                if(f_Client == null) f_Client = new TcpClient();
                //Подключаем клиента к удалённому серверу
                f_Client.Connect(fHost, fPort);

                //Получаем сетевой поток
                f_ClientStream = f_Client.GetStream();
                f_Client.ReceiveBufferSize = 102400;
                //Создаём объект для чтения из сетевого потока
                f_Reader = new BinaryReader(f_ClientStream);
                //...и для записи в сетевой поток
                f_Writer = new BinaryWriter(f_ClientStream);

                //Создаём основной поток клиента
                f_CheckThread = new Thread(new ThreadStart(CheckConnection));
                f_CheckThread.Name = string.Format("Поток TCP клиента {0} : {1}", fHost, fPort);
                //Делаем поток не основным
                f_CheckThread.IsBackground = true;
                //Запускаем основной поток клиента
                f_CheckThread.Start();

                //сообщаем о подключении
                InvokeConnectedEvent();
            }
            //В случае возникновения ошибки сокета
            catch(SocketException SocketError)
            {
                //Сбрасываем флаг подключения
                f_Connected = false;
                //Делаем непонятные телодвижения в плане анализ кодов ошибок... зачем - непонятно...
                if(SocketError.ErrorCode == 10061)
                {
                    InvokeDisconnectedEvent();
                    InvokeErrorEvent(SocketError);
                }
                else
                {
                    //Сообщаем о появлении ошибки
                    InvokeErrorEvent(SocketError);
                }
            }
        }

        /// <summary>Метод остановки клиента</summary>
        protected void Stop()
        {
            //Если связь не была установлена, то возврат
            if(!Connected) return;
            //Сбрасываем флаг подключения
            f_Connected = false;

            //Начинаем асинхронный процесс отключения
            f_Client.Client.BeginDisconnect(false, new AsyncCallback(DisconnectionComplite), f_Client.Client);
        }

        /// <summary>Метод асинхронного завершения процесса отключения от сервера</summary>
        protected void DisconnectionComplite(IAsyncResult AsyncResult)
        {
            //Получаем сокет клиента
            Socket lv_ClientSocket = (Socket)AsyncResult.AsyncState;
            try
            {
                //Пытаемся его отключить
                lv_ClientSocket.EndDisconnect(AsyncResult);

                //Если главный поток клиента ещё активен
                if(f_CheckThread != null)
                {
                    f_Reader.Close();
                    if(f_CheckThread.IsAlive)
                        f_CheckThread.Abort(); //Останавливаем поток
                    f_CheckThread.Join(); //Ожидаем завершения
                    f_CheckThread = null; //Убираем ссылку на главный поток (отдаём его на растерзание сборщику мусора)
                }

                //Закрываем поток данных
                f_ClientStream.Close();

                f_Reader = null;
                f_Writer = null;

                f_ClientStream = null;

                //Убираем ссылку на клиента
                f_Client = null;

                //Сообщаем об отключении
                InvokeDisconnectedEvent();

            }
            //В случае ошибки
            catch(Exception Error)
            {
                //Сообщаем о ошибке
                InvokeErrorEvent(Error);
            }
        }

        #endregion

        /* - Отправка / получение данных и проверка соединения ------------------------------------------------------------------------------- */

        #region Отправка / получение

        /// <summary>
        /// Главный метод клиента. Стартует в отдельном потоке. 
        /// Проверяет наличие данных от сервера и связь с ним.
        /// </summary>
        protected void CheckConnection()
        {
            if(Thread.CurrentThread.Name == null)
                Thread.CurrentThread.Name = "Listner " + this.Host + ":" + this.Port;
            //Читаем из потока до тех пор, пока не будет сброшен флаг подключения (сверху не решат отключиться)
            //или пока не будет получено "ничто" - это происходит, когда удалённый сервер порвал соединение
            try
            {

                byte[] lv_ReceiveBuffer = new byte[1];
                bool lv_DataAvalable = true;
                while(this.Connected && (lv_ReceiveBuffer.Length != 0 || lv_DataAvalable))
                {
                    lv_ReceiveBuffer = f_Reader.ReadBytes(f_Client.Available);
                    lv_DataAvalable = f_Client.Available != 0;

                    //Если что-то получино, и это что-то имеет длину, большую 0-я, то сообщаем об этом
                    if(lv_ReceiveBuffer != null)
                        if(lv_ReceiveBuffer.Length > 0)
                            InvokeReceiveDataEvent(lv_ReceiveBuffer);
                }
            }
            catch(ThreadAbortException) { }
            catch(IOException) { }

            //После выпадения из цыкла (значит соединение было разорвано) вызываем метод отключения клиента.
            //Просто так его вызвать нельзя потому, что тогда поток будет ждатье го завершшения, а в это метода
            //происходит остановка этого потока с его синхронизацией. В итоге происходит мёртвая блокировка.
            //Во избежании этого мы запускаем метод отключения в отдельном потоке, а этот благополучно завершается.
            new Thread(new ThreadStart(Stop)).Start();

            //В случае если соединение рвётся на нашей стороне по команде пользователя, то метод отключения уже инициирован, 
            //но мы его запускаем ещё раз. Это делать можно потому, что поток будет остановлен тем мтодом в момент
            //когда уже будет сброшен флаг активности клиента и это не позволит повторно войти в метод остановки.
        }

        /* ----------------------------------------------------------------------------------------------------------------------------------- */

        /// <summary>Метод отправки данных на сервер</summary>
        /// <param name="Message">Отправляемые данные</param>
        public virtual void Send(string Message)
        {
            this.Send(DataEncoding.GetBytes(Message));
        }

        public virtual void Send(byte[] Data)
        {
            if(!Connected) return;

            lock(f_ClientStream)
            {
                try
                {
                    f_Writer.Write(Data);
                    f_Writer.Flush();

                    InvokeDataSendEvent(Data);
                }
                catch(SocketException lv_SocketError)
                {
                    if(lv_SocketError.ErrorCode == 10053)
                    {
                        Stop();
                        InvokeDisconnectedEvent();
                    }
                    else
                    {
                        Stop();
                        InvokeErrorEvent(lv_SocketError);
                    }
                }
                catch(IOException) { Stop(); InvokeDisconnectedEvent(); }
                catch(Exception Error) { InvokeErrorEvent(Error); }
            }
        }

        public static bool CheckSerializable(object Object)
        {
            return Attribute.GetCustomAttribute(Object.GetType(), typeof(SerializableAttribute)) != null;
        }

        public virtual void SendObject(object Object)
        {
            using(MemoryStream lv_Stream = new MemoryStream())
            {
                DataFormatter.Serialize(lv_Stream, Object);
                Send(lv_Stream.ToArray());
            }
        }

        #endregion

        /* ----------------------------------------------------------------------------------------------------------------------------------- */

        public override string ToString() { return Host + ":" + Port.ToString(); }

        public static implicit operator TcpClient(CTCPClient Client)
        {
            return Client.f_Client;
        }

        public static implicit operator CTCPClient(TcpClient Client)
        {
            return new CTCPClient(Client);
        }

        #region IDisposable Members

        public void Dispose()
        {
            this.Stop();
        }

        void IDisposable.Dispose()
        {
            this.Dispose();
        }

        #endregion
    }
}
